Skip to content

Move git config/remote to gitrepo package and add global lock to resolve possible conflict when updating repository git config file #35151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

lunny
Copy link
Member

@lunny lunny commented Jul 24, 2025

Partially fix #32018

git config and git remote write operations create a temporary file named config.lock. Since these operations are not atomic, they must not be run in parallel. If two requests attempt to modify the same repository concurrently—such as during a compare operation—one may fail due to the presence of an existing config.lock file.

In cases where config.lock is left behind due to an unexpected program exit, a global lock mechanism could allow us to safely remove the stale lock file when a related error is detected. While this behavior is not yet implemented in this PR, it is planned for a future enhancement.

…lve possible conflict when updating repository git config file
@lunny lunny added the type/bug label Jul 24, 2025
@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Jul 24, 2025
@github-actions github-actions bot added modifies/api This PR adds API routes or modifies them modifies/go Pull requests that update Go code labels Jul 24, 2025
@wxiaoguang
Copy link
Contributor

No, it doesn't fix #32018

Copy link
Contributor

@wxiaoguang wxiaoguang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't see why it is worth or right to do so, and don't see why it fixes that issue

@GiteaBot GiteaBot added lgtm/blocked A maintainer has reservations with the PR and thus it cannot be merged and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels Jul 24, 2025
@lunny
Copy link
Member Author

lunny commented Jul 24, 2025

Don't see why it is worth or right to do so, and don't see why it fixes that issue

git config and git remote write operations create a temporary file named config.lock. Since these operations are not atomic, they must not be run in parallel. If two requests attempt to modify the same repository concurrently—such as during a compare operation—one may fail due to the presence of an existing config.lock file.

In cases where config.lock is left behind due to an unexpected program exit, a global lock mechanism could allow us to safely remove the stale lock file when a related error is detected. While this behavior is not yet implemented in this PR, it is planned for a future enhancement.

@wxiaoguang
Copy link
Contributor

wxiaoguang commented Jul 25, 2025

So it is an improvement but doesn't "Fix #32018", right?

But does it really need the "global lock"? Is it possible that git already uses "*.lock" files by flock for global lock? Then Gitea shouldn't do anything more?

@lunny
Copy link
Member Author

lunny commented Jul 25, 2025

So it is an improvement but doesn't "Fix #32018", right?

But does it really need the "global lock"? Is it possible that git already uses "*.lock" files by flock for global lock? Then Gitea shouldn't do anything more?

The Git command-line interface is primarily designed for human interaction. When a git config write operation is initiated and a config.lock file is created, any subsequent git config write operation will immediately fail instead of waiting for the first one to complete.

While I couldn’t find official documentation confirming this behavior, the implementation can be found in the Git source code here:
https://github.com/git/git/blob/97e14d99f6def189b0f786ac6207b792ca3197b1/config.c#L3199

Based on this, it appears that Git does not implement a locking mechanism that causes subsequent operations to wait—it simply fails fast.

I believe this change partially addresses #32018, as there are three possible causes for the issue:

  • Concurrent requests attempting to compare between the same base and forked repositories.
  • A Git command exits unexpectedly, leaving behind a config.lock file. I plan to implement automatic cleanup of stale config.lock files in another PR, which would help resolve this case.
  • An administrator manually runs Git commands on the Gitea server. This scenario is outside the scope of this PR, and I don’t believe it’s something we can reliably handle within the application.

@wxiaoguang
Copy link
Contributor

wxiaoguang commented Jul 25, 2025

Git already does a file-based "global lock"

https://github.com/git/git/blob/master/lockfile.c#L104 (config.c hold_lock_file_for_update -> lockfile.c lock_file_timeout )

I do not think this PR is worth or right.


Update: git passes timeout=0, so it only locks "once"

@wxiaoguang wxiaoguang dismissed their stale review July 25, 2025 02:28

dismiss

@GiteaBot GiteaBot added lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. and removed lgtm/blocked A maintainer has reservations with the PR and thus it cannot be merged labels Jul 25, 2025
@wxiaoguang wxiaoguang marked this pull request as draft July 25, 2025 03:15
@lunny lunny marked this pull request as ready for review July 26, 2025 18:06
@lunny lunny requested a review from wxiaoguang July 26, 2025 18:06
@GiteaBot GiteaBot added lgtm/need 1 This PR needs approval from one additional maintainer to be merged. and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels Jul 31, 2025
@lunny lunny added this to the 1.25.0 milestone Jul 31, 2025
@lunny
Copy link
Member Author

lunny commented Aug 19, 2025

last call @go-gitea/technical-oversight-committee

// No lock is needed because the remote remoteName will be checked before invoking this function.
// Then it will not update the remote automatically if the remote does not exist.
func GitRemoteUpdatePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error {
return git.NewCommand("remote", "update", "--prune").AddDynamicArguments(remoteName).
Copy link
Contributor

@wxiaoguang wxiaoguang Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and above, why git remote update/prune belongs to "config.go"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 120 to 124
releaser, err := globallock.Lock(ctx, getRepoConfigLockKey(repo.RelativePath()))
if err != nil {
return err
}
defer releaser()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these "lock" related code could be in a helper wrapper function:

gitConfigLock(..., func() {
   ...
})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 113 to 115
if addr == "" {
return nil, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this design is problematic.

Developers have been used to that "if err == nil, then there is a non-nil return value"

If you do so, developers would forget to check "u == nil" frequently, and the nil check introduces unnecessary code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err != nil && !git.IsRemoteNotExistError(err) {
return err
}

cmd := git.NewCommand("remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(addr)
_, _, err = cmd.RunStdString(ctx, &git.RunOpts{Dir: repoPath})
err = gitrepo.GitRemoteAdd(ctx, repo, remoteName, addr, "--mirror=fetch")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and other places, IIRC you have defined consts, but why still use string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stderr: &stderrBuilder,
}); err != nil {

// check whether the remote still exists before pruning to avoid prune creating a new remote
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to avoid prune creating a new remote

What "prune"? "git remote prune" or "git remote update --prune"?

I see neither of them would "create a new remote" in the git manual. Correct me if I was wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 368 to 369
// check whether the remote still exists before remote update prune
// this is needed because prune will not fail if the remote does not exist
Copy link
Contributor

@wxiaoguang wxiaoguang Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What "prune"? "git remote prune" or "git remote update --prune"?

  • below does git remote update --prune
  • pruneBrokenReferences does git remote prune

So what are you doing here for?


I see NEITHER of them would "create a new remote" in the git manual. Correct me if I was wrong.

Why "this is needed because prune will not fail if the remote does not exist" is a problem?

Why you need this logic?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It said before remote update prune ..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It said before remote update prune ..

Why it needs this logic? What wrong would happen if remote doesn't exist?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the comment via 38c19d2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it needs this logic? What wrong would happen if remote doesn't exist?

Why the "remote" might not exist?

What's wrong with "it will appear to succeed silently, even though nothing was updated."?

If you return "false" here, what end users can see or can do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remove these lines via a9633fd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lgtm/need 1 This PR needs approval from one additional maintainer to be merged. modifies/api This PR adds API routes or modifies them modifies/go Pull requests that update Go code type/bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GetCompareInfo, AddRemote: exit status 128 - error: could not lock config file config: File exists
4 participants